import sys, random, copy, math, os
from bresenham import bresenham as bresenham_line

from PyQt5.QtGui import QColor, QIcon
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import *

from protolib import rgb_to_hex, pad_or_truncate, get_app_root
from protolib.obj_buffer import UndoRedoBuffer
from protolib.gui_utils.pyqt5_messageboxes import show_critical_messagebox, show_question_messagebox
from protolib.gui_utils.pyqt5_widgets.pixel_canvas import PixelCanvas as PixelCanvasFromProtoLib
from protolib.gui_utils.pyqt5_widgets.color_picker import ColorPickerWidget

from strange_io.pixels import StrangePixels
from strange_io.colors import StrangeColors
from strange_io.pixels.to_pil_image import convert as convert_spixels

from bresenham_circle import generate_circle_coordinates as bresenham_circle
from flood4 import floodFill as flood_fill_4_directions
from flood8 import floodFill as flood_fill_8_directions

CANVAS_WIDTH = 16
CANVAS_HEIGHT = 16
CANVAS_PIXEL_SIZE = 20
transperency_index = 8

# Patch
class PixelCanvas(PixelCanvasFromProtoLib):
    def sizeHint(self):
        return QSize(
            self.canvasWidth * self.pixel_size,
            self.canvasHeight * self.pixel_size
        )

class NewFileDialog(QDialog):
    def __init__(self, default_width=CANVAS_WIDTH, default_height=CANVAS_HEIGHT):
        super().__init__()

        self.setWindowTitle('New file')

        self.returnVals = {}

        self.canvasWidth_label = QLabel('Width:')
        self.canvasWidth_spinbox = QSpinBox(self)
        self.canvasWidth_spinbox.setMinimum(1)
        self.canvasWidth_spinbox.setMaximum(256)
        self.canvasWidth_spinbox.setValue(default_width)

        self.canvasHeight_label = QLabel('Height:')
        self.canvasHeight_spinbox = QSpinBox(self)
        self.canvasHeight_spinbox.setMinimum(1)
        self.canvasHeight_spinbox.setMaximum(256)
        self.canvasHeight_spinbox.setValue(default_height)

        self.submit_button = QPushButton('Submit', self)
        self.submit_button.clicked.connect(self.submit_form)

        self.canvasWidth_widget = QWidget()
        layout_width = QHBoxLayout()
        layout_width.addWidget(self.canvasWidth_label)
        layout_width.addWidget(self.canvasWidth_spinbox)
        self.canvasWidth_widget.setLayout(layout_width)

        self.canvasHeight_widget = QWidget()
        layout_height = QHBoxLayout()
        layout_height.addWidget(self.canvasHeight_label)
        layout_height.addWidget(self.canvasHeight_spinbox)
        self.canvasHeight_widget.setLayout(layout_height)

        layout = QVBoxLayout()
        layout.addWidget(self.canvasWidth_widget)
        layout.addWidget(self.canvasHeight_widget)
        layout.addWidget(self.submit_button)
        self.setLayout(layout)

    def submit_form(self):
        self.returnVals['width'] = self.canvasWidth_spinbox.value()
        self.returnVals['height'] = self.canvasHeight_spinbox.value()
        self.accept()

    @staticmethod
    def show_form(default_width=CANVAS_WIDTH, default_height=CANVAS_HEIGHT):
        dialog = NewFileDialog(default_width=default_width, default_height=default_height)
        result = dialog.exec_()
        if result:
            return dialog.returnVals['width'], dialog.returnVals['height']

class MainWindow(QMainWindow):
    def __init__(self, args):
        super().__init__()

        self.setWindowIcon(QIcon('icon.png'))
        self.setWindowTitle("Strange Pixels Editor - Strange I/O")
        self.setMinimumSize(380, 450)

        #region
        menuBar = self.menuBar()
        canvasToolBar = QToolBar("Canvas Tool Bar")
        self.addToolBar(Qt.LeftToolBarArea, canvasToolBar)
        #region
        self.fileMenu = QMenu("File", self)
        menuBar.addMenu(self.fileMenu)
        #region
        newFileAction = QAction("New", self)
        newFileAction.setShortcut("Ctrl+N")
        newFileAction.setStatusTip('Create new file and clear change buffer')
        newFileAction.triggered.connect(self.new_file)
        self.fileMenu.addAction(newFileAction)
        #endregion
        #region
        openFileAction = QAction("Open", self)
        openFileAction.setShortcut("Ctrl+O")
        openFileAction.setStatusTip('Load a file from disk')
        openFileAction.triggered.connect(self.user_open_file)
        self.fileMenu.addAction(openFileAction)
        #endregion
        #region
        saveAction = QAction("Save", self)
        saveAction.setShortcut("Ctrl+S")
        saveAction.setStatusTip('Save the file buffer to disk')
        saveAction.triggered.connect(self.save_file)
        self.fileMenu.addAction(saveAction)
        #endregion
        #region
        saveAsAction = QAction("Save As", self)
        saveAsAction.setShortcut("Ctrl+Shift+S")
        saveAsAction.setStatusTip('Change current file and save the file buffer to disk')
        saveAsAction.triggered.connect(self.save_as_file)
        self.fileMenu.addAction(saveAsAction)
        #endregion
        #region
        exportAction = QAction("Export", self)
        exportAction.setShortcut("Ctrl+E")
        exportAction.setStatusTip('Convert the loaded image and palette to a PIL image that can be saved as a more common format')
        exportAction.triggered.connect(self.user_export_file)
        self.fileMenu.addAction(exportAction)
        #endregion
        self.fileMenu.addSeparator()
        #region
        quitAction = QAction("Quit", self)
        quitAction.setShortcut("Ctrl+Q")
        quitAction.setStatusTip('Leave The App')
        quitAction.triggered.connect(self.close)
        self.fileMenu.addAction(quitAction)
        #endregion
        #endregion
        #region
        self.editMenu = QMenu("Edit", self)
        menuBar.addMenu(self.editMenu)
        #region
        undoAction = QAction("Undo", self)
        undoAction.setShortcut("Ctrl+Z")
        undoAction.setStatusTip('Subtract one from the change buffer\'s state pointer and set the canvas content to the change buffer state')
        undoAction.triggered.connect(self.undo_last_change)
        self.editMenu.addAction(undoAction)
        #endregion
        #region
        redoAction = QAction("Redo", self)
        redoAction.setShortcut("Ctrl+Y")
        redoAction.setStatusTip('Add one from the change buffer\'s state pointer and set the canvas content to the change buffer state')
        redoAction.triggered.connect(self.redo_last_change)
        self.editMenu.addAction(redoAction)
        #endregion
        #endregion
        #region
        self.paletteMenu = QMenu("Palette", self)
        menuBar.addMenu(self.paletteMenu)
        #region
        openPaletteAction = QAction("Open", self)
        openPaletteAction.setShortcut("Ctrl+Shift+O")
        openPaletteAction.setStatusTip('Open a strange colors palette from disk')
        openPaletteAction.triggered.connect(self.user_load_palette)
        self.paletteMenu.addAction(openPaletteAction)
        #endregion
        #region
        reloadPaletteAction = QAction("Reload", self)
        reloadPaletteAction.setShortcut("Ctrl+Shift+R")
        reloadPaletteAction.setStatusTip('Reload the last opened strange colors palette from disk')
        reloadPaletteAction.triggered.connect(self.load_palette)
        self.paletteMenu.addAction(reloadPaletteAction)
        #endregion
        #endregion
        #region
        self.toolMenu = QMenu("Tool", self)
        menuBar.addMenu(self.toolMenu)
        #region
        setNoneToolAction = QAction("None", self, checkable=True)
        setNoneToolAction.setStatusTip('Set the current tool to \'none\'')
        setNoneToolAction.setShortcut("N")
        setNoneToolAction.triggered.connect(lambda: self.setCanvasTool('none'))
        setNoneToolAction.setIcon(QIcon(os.path.join(get_app_root(), 'assets', 'tool', 'none.svg')))
        self.toolMenu.addAction(setNoneToolAction)
        canvasToolBar.addAction(setNoneToolAction)
        #endregion
        #region
        setBrushToolAction = QAction("Brush", self, checkable=True)
        setBrushToolAction.setStatusTip('Set the current tool to \'brush\'')
        setBrushToolAction.setShortcut("B")
        setBrushToolAction.triggered.connect(lambda: self.setCanvasTool('brush'))
        setBrushToolAction.setIcon(QIcon(os.path.join(get_app_root(), 'assets', 'tool', 'brush.svg')))
        self.toolMenu.addAction(setBrushToolAction)
        canvasToolBar.addAction(setBrushToolAction)
        #endregion
        #region
        setEraseToolAction = QAction("Erase", self, checkable=True)
        setEraseToolAction.setStatusTip('Set the current tool to \'erase\'')
        setEraseToolAction.setShortcut("E")
        setEraseToolAction.triggered.connect(lambda: self.setCanvasTool('erase'))
        setEraseToolAction.setIcon(QIcon(os.path.join(get_app_root(), 'assets', 'tool', 'erase.svg')))
        self.toolMenu.addAction(setEraseToolAction)
        canvasToolBar.addAction(setEraseToolAction)
        #endregion
        #region
        setLineToolAction = QAction("Line", self, checkable=True)
        setLineToolAction.setStatusTip('Set the current tool to \'line\'')
        setLineToolAction.setShortcut("L")
        setLineToolAction.triggered.connect(lambda: self.setCanvasTool('line'))
        setLineToolAction.setIcon(QIcon(os.path.join(get_app_root(), 'assets', 'tool', 'line.svg')))
        self.toolMenu.addAction(setLineToolAction)
        canvasToolBar.addAction(setLineToolAction)
        #endregion
        #region
        setCircleToolAction = QAction("Circle", self, checkable=True)
        setCircleToolAction.setStatusTip('Set the current tool to \'circle\'')
        setCircleToolAction.setShortcut("C")
        setCircleToolAction.triggered.connect(lambda: self.setCanvasTool('circle'))
        setCircleToolAction.setIcon(QIcon(os.path.join(get_app_root(), 'assets', 'tool', 'circle.svg')))
        self.toolMenu.addAction(setCircleToolAction)
        canvasToolBar.addAction(setCircleToolAction)
        #endregion
        #region
        setFlood4ToolAction = QAction("Flood 4", self, checkable=True)
        setFlood4ToolAction.setStatusTip('Set the current tool to \'flood4\'')
        setFlood4ToolAction.setShortcut("G")
        setFlood4ToolAction.triggered.connect(lambda: self.setCanvasTool('flood4'))
        setFlood4ToolAction.setIcon(QIcon(os.path.join(get_app_root(), 'assets', 'tool', 'flood4.svg')))
        self.toolMenu.addAction(setFlood4ToolAction)
        canvasToolBar.addAction(setFlood4ToolAction)
        #endregion
        #region
        setFlood8ToolAction = QAction("Flood 8", self, checkable=True)
        setFlood8ToolAction.setStatusTip('Set the current tool to \'flood8\'')
        setFlood8ToolAction.setShortcut("F")
        setFlood8ToolAction.triggered.connect(lambda: self.setCanvasTool('flood8'))
        setFlood8ToolAction.setIcon(QIcon(os.path.join(get_app_root(), 'assets', 'tool', 'flood8.svg')))
        self.toolMenu.addAction(setFlood8ToolAction)
        canvasToolBar.addAction(setFlood8ToolAction)
        #endregion
        setBrushToolAction.setChecked(True)
        #region
        toolGroup = QActionGroup(self)
        toolGroup.addAction(setNoneToolAction)
        toolGroup.addAction(setBrushToolAction)
        toolGroup.addAction(setEraseToolAction)
        toolGroup.addAction(setLineToolAction)
        toolGroup.addAction(setCircleToolAction)
        toolGroup.addAction(setFlood4ToolAction)
        toolGroup.addAction(setFlood8ToolAction)
        #endregion
        #endregion
        #endregion

        #region
        layout = QVBoxLayout()

        self.widget = QWidget()
        self.widget.setLayout(layout)

        self.canvas_scroll_area = QScrollArea()
        self.canvas_scroll_area.setAlignment(Qt.AlignHCenter|Qt.AlignVCenter)
        # self.canvas_scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
        # self.canvas_scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
        self.canvas = PixelCanvas(self, CANVAS_WIDTH, CANVAS_HEIGHT, CANVAS_PIXEL_SIZE, self.canvasClickCallback, self.canvasDragCallback, self.canvasMouseReleaseCallback)
        self.canvas_scroll_area.setWidget(self.canvas)
        layout.addWidget(self.canvas_scroll_area)

        self.color_picker = ColorPickerWidget(self)
        color_frame_size = int(self.color_picker.color_combo.height() * 0.68)
        self.color_picker.color_frame.setFixedSize(color_frame_size, color_frame_size)
        layout.addWidget(self.color_picker)

        self.save_button = QPushButton()
        self.save_button.setText('Save')
        self.save_button.clicked.connect(self.save_file)
        layout.addWidget(self.save_button)
        #endregion

        #region
        self.spixels = StrangePixels()
        self.scolors = StrangeColors()
        self.edited = False
        self.current_file = None
        self.canvasTool = 'brush'
        self.canvas_pixel_color_indexes = []
        self.updateCanvasSize()
        self.change_buffer = UndoRedoBuffer(copy.deepcopy(self.canvas_pixel_color_indexes))
        #endregion

        self.setCentralWidget(self.widget)
        self.canvas.clear()

        if args.palette:
            try:
                self.load_palette(args.palette)
            except OSError as e:
                show_critical_messagebox(e.__class__.__name__, str(e))
        if args.file_path:
            try:
                self.open_file(args.file_path)
            except OSError as e:
                show_critical_messagebox(e.__class__.__name__, str(e))

        self.previous_circle_radius = 0

    def open_file(self, file_path):
        self.canvas.clear()
        self.spixels.load_file(file_path)
        if self.spixels.palette_size > len(self.color_picker.color_palette):
            response = show_question_messagebox(message='The file you\'re trying to load uses a palette that is longer than the currently loaded palette. Would you like to extend to loaded palette? If not, any missing colors will be erased.', buttons=QMessageBox.Yes|QMessageBox.No)
            if response == QMessageBox.Yes:
                missing_colors = 0
                if self.spixels.palette_size > len(self.color_picker.color_palette):
                    missing_colors = self.spixels.palette_size - len(self.color_picker.color_palette)
                for _ in range(missing_colors):
                    color = (random.randrange(0, 255), random.randrange(0, 255), random.randrange(0, 255))
                    self.color_picker.color_palette.append(QColor(*color))
                    self.color_picker.color_combo.addItem(rgb_to_hex(color).upper())
        self.canvas.canvasWidth = self.spixels.width
        self.canvas.canvasHeight = self.spixels.height
        self.updateCanvasSize()
        for x, column in enumerate(self.spixels.image):
            for y, pixel in enumerate(column):
                if pixel != transperency_index and pixel < len(self.color_picker.color_palette):
                    self.canvas.drawPixel(x, y, self.color_picker.color_palette[pixel])
                    self.canvas_pixel_color_indexes[x][y] = pixel
                elif pixel == transperency_index:
                    self.canvas.erasePixel(x, y)
                    self.canvas_pixel_color_indexes[x][y] = transperency_index
        self.change_buffer = UndoRedoBuffer(copy.deepcopy(self.canvas_pixel_color_indexes))

    def user_open_file(self):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        file_path, _ = QFileDialog.getOpenFileName(self, "Open file", "", "Strange Pixels Image (*.spixels);;Strange I/O Documents (*.strange *.sio);;All Files (*)", options=options)
        if file_path:
            self.current_file = file_path
            self.open_file(file_path)

    def save_file(self):
        if self.current_file:
            try:
                document = {
                    'palette_size': len(self.color_picker.color_palette),
                    'height': self.canvas.canvasHeight,
                    'width': self.canvas.canvasWidth,
                    'image': [[transperency_index for _ in range(self.canvas.canvasHeight)] for _ in range(self.canvas.canvasWidth)]
                }
                for x, column in enumerate(self.canvas_pixel_color_indexes):
                    for y, pixel in enumerate(column):
                        document['image'][x][y] = pixel
                self.spixels.load(document)
                self.spixels.dump_file(self.current_file)
            except (ValueError, OSError) as e:
                show_critical_messagebox(e.__class__.__name__, str(e))
            self.edited = False
        else:
            self.save_as_file()

    def save_as_file(self):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        file_path, _ = QFileDialog.getSaveFileName(self, "Save file", "", "Strange Pixels Image (*.spixels);;Strange I/O Documents (*.strange *.sio);;All Files (*)", options=options)
        if file_path:
            try:
                self.current_file = file_path
                self.save_file()
            except (ValueError, OSError) as e:
                show_critical_messagebox(e.__class__.__name__, str(e))
            self.edited = False

    def user_export_file(self):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        file_path, _ = QFileDialog.getSaveFileName(self, "Export file", "", "PNG Image (*.png);;JPEG Image (*.jpeg *.jpg);;All Files (*)", options=options)
        if file_path:
            try:
                self.current_file = file_path
                self.export_file(file_path)
            except (ValueError, OSError) as e:
                show_critical_messagebox(e.__class__.__name__, str(e))

    def export_file(self, path):
        scolors = (
            [(color.getRgb()[0], color.getRgb()[1], color.getRgb()[2]) for color in self.color_picker.color_palette],
            transperency_index
        )
        scolors[0].append((random.randrange(0, 255), random.randrange(0, 255), random.randrange(0, 255)))
        self.scolors.load(scolors)
        pil_image = convert_spixels(self.spixels, self.scolors, True)
        pil_image.save(path)

    def new_file(self):
        form_result = NewFileDialog.show_form(self.canvas.canvasWidth, self.canvas.canvasHeight)
        if form_result:
            self.canvas.canvasWidth = form_result[0]
            self.canvas.canvasHeight = form_result[1]
            self.canvas.clear()
            self.current_file = None
            self.updateCanvasSize()
        self.change_buffer = UndoRedoBuffer(copy.deepcopy(self.canvas_pixel_color_indexes))

    def user_load_palette(self):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        file_path, _ = QFileDialog.getOpenFileName(self, "Open file", "", "Strange Colors Palette (*.scolors);;Strange I/O Documents (*.strange *.sio);;All Files (*)", options=options)
        if file_path:
            self.palette_file = file_path
            self.load_palette(file_path)

    def load_palette(self, file_path):
        global transperency_index
        current_palette_length = len(self.color_picker.color_palette)
        current_transperency_index = copy.copy(transperency_index)
        self.scolors.load_file(file_path)
        if len(self.scolors) < current_palette_length:
            response = show_question_messagebox('Extend palette', 'The palette you\'re trying to load is shorter than the currently loaded palette. Would you like to extend the loaded palette? If not, any pixels with that use a color not available in the newly loaded palette will be erased.')
            if response == QMessageBox.Ok:
                erase_missing_colors = True
            else:
                erase_missing_colors = False
        else:
            erase_missing_colors = False
        for _ in range(current_palette_length):
            self.color_picker.color_combo.removeItem(0)
        self.color_picker.color_palette.clear()
        transperency_index = self.scolors.transperent_index
        for index, color in enumerate(self.scolors):
            if self.scolors.transperent_index == index:
                continue
            self.color_picker.color_palette.append(QColor(*color))
            self.color_picker.color_combo.addItem(rgb_to_hex(color).upper())
        self.color_picker.color_combo.setCurrentIndex(0)
        self.refreshCanvas(current_transperency_index, erase_missing_colors)

    def canvasClickCallback(self, event, x, y):
        self.paintCanvas(event, x, y)

    def canvasDragCallback(self, event, x, y):
        if self.canvasTool == 'line':
            start = self.canvas.last_mouse_click_point
            self.canvasDrawLinePreview(start, (x, y), self.color_picker.getColorIndex())
        elif self.canvasTool == 'circle':
            point = self.canvas.last_mouse_click_point
            radius = int(math.sqrt(((x - point[0]) ** 2) + ((y - point[1]) ** 2)))
            self.canvasDrawCirclePreview(point, radius, self.color_picker.getColorIndex())
        else:
            self.paintCanvas(event, x, y)
        self.previous_cursor_location = (x, y)

    def canvasMouseReleaseCallback(self, event, x, y):
        match self.canvasTool:
            case 'line':
                start = self.canvas.last_mouse_click_point
                end = self.canvas.last_mouse_release_point
                self.canvasDrawLine(start, end, self.color_picker.getColorIndex())
                self.refreshCanvas(transperency_index)
            case 'circle':
                start = self.canvas.last_mouse_click_point
                end = self.canvas.last_mouse_release_point
                radius = int(math.sqrt(((end[0] - start[0]) ** 2) + ((end[1] - start[1]) ** 2)))
                self.canvasDrawCircle(start, radius, self.color_picker.getColorIndex())
                self.refreshCanvas(transperency_index)
            case _:
                if self.canvasTool in ['brush', 'erase', 'line', 'circle', 'flood4', 'flood8']:
                    self.change_buffer.add_state(copy.deepcopy(self.canvas_pixel_color_indexes))

    def paintCanvas(self, event, x, y):
        color = self.color_picker.getColorIndex()
        if self.canvasTool == 'brush':
            self.canvasDrawPixel(x, y, color)
        elif self.canvasTool == 'erase':
            self.canvasErasePixel(x, y)
        elif self.canvasTool == 'flood4':
            self.canvas4DirectionFlood(x, y, color)
        elif self.canvasTool == 'flood8':
            self.canvas8DirectionFlood(x, y, color)

    def canvasDrawLinePreview(self, start, end, color):
        self.canvas_pixel_color_indexes = copy.deepcopy(self.change_buffer.get_state())
        self.canvasDrawLine(start, end, color)
        self.refreshCanvas(transperency_index)

    def canvasDrawCirclePreview(self, point, radius, color):
        if self.previous_circle_radius != radius:
            self.canvas_pixel_color_indexes = copy.deepcopy(self.change_buffer.get_state())
            self.canvasDrawCircle(point, radius, color)
            self.refreshCanvas(transperency_index)
        self.previous_circle_radius = radius

    def canvasDrawLine(self, start, end, color):
        line = bresenham_line(*start, *end)
        for point in line:
            if self.checkValidPixelCoords(*point):
                self.canvas_pixel_color_indexes[point[0]][point[1]] = color

    def canvasDrawCircle(self, center, radius, color):
        circle = list(bresenham_circle(radius))
        for point in circle:
            if self.checkValidPixelCoords(point[0] + center[0], point[1] + center[1]):
                self.canvas_pixel_color_indexes[point[0] + center[0]][point[1] + center[1]] = color

    def canvas4DirectionFlood(self, x, y, color):
        if self.checkValidPixelCoords(x, y):
            flood_fill_4_directions(self.canvas_pixel_color_indexes, len(self.canvas_pixel_color_indexes), len(self.canvas_pixel_color_indexes[0]), x, y, self.canvas_pixel_color_indexes[x][y], color)
            self.refreshCanvas(transperency_index)

    def canvas8DirectionFlood(self, x, y, color):
        if self.checkValidPixelCoords(x, y):
            flood_fill_8_directions(self.canvas_pixel_color_indexes, len(self.canvas_pixel_color_indexes), len(self.canvas_pixel_color_indexes[0]), x, y, self.canvas_pixel_color_indexes[x][y], color)
            self.refreshCanvas(transperency_index)

    def canvasDrawPixel(self, x, y, color):
        if self.checkValidPixelCoords(x, y):
            self.canvas.drawPixel(x, y, self.color_picker.color_palette[color])
            self.canvas_pixel_color_indexes[x][y] = color

    def canvasErasePixel(self, x, y):
        if self.checkValidPixelCoords(x, y):
            self.canvas.erasePixel(x, y)
            self.canvas_pixel_color_indexes[x][y] = transperency_index

    def setCanvasTool(self, tool):
        self.canvasTool = tool

    def checkValidPixelCoords(self, x, y):
        return x < self.canvas.canvasWidth and y < self.canvas.canvasHeight and x >= 0 and y >= 0

    def updateCanvasSize(self):
        self.canvas.updateSize()
        self.canvas_pixel_color_indexes = pad_or_truncate(self.canvas_pixel_color_indexes, self.canvas.canvasWidth)
        for row, _ in enumerate(self.canvas_pixel_color_indexes):
            if self.canvas_pixel_color_indexes[row] == None:
                self.canvas_pixel_color_indexes[row] = [transperency_index for _ in range(self.canvas.canvasHeight)]
            else:
                self.canvas_pixel_color_indexes[row] = pad_or_truncate(self.canvas_pixel_color_indexes[row], self.canvas.canvasHeight)
                for index, _ in enumerate(self.canvas_pixel_color_indexes[row]):
                    if self.canvas_pixel_color_indexes[row][index] == None:
                        self.canvas_pixel_color_indexes[row][index] = transperency_index

    def refreshCanvas(self, color_to_erase, erase_missing_colors=False):
        for x, column in enumerate(self.canvas_pixel_color_indexes):
            for y, color in enumerate(column):
                if color == color_to_erase:
                    self.canvas.erasePixel(x, y)
                else:
                    if color in self.color_picker.color_palette or not erase_missing_colors:
                        self.canvas.drawPixel(x, y, self.color_picker.color_palette[color])
                    else:
                        self.canvas.erasePixel(x, y)

    def undo_last_change(self):
        self.change_buffer.undo()
        self.canvas_pixel_color_indexes = self.change_buffer.get_state()
        self.refreshCanvas(transperency_index)

    def redo_last_change(self):
        self.change_buffer.redo()
        self.canvas_pixel_color_indexes = self.change_buffer.get_state()
        self.refreshCanvas(transperency_index)

def main(args):
    app = QApplication(sys.argv)
    window = MainWindow(args)
    window.show()
    app.exec()
